Fix ISCP index consumer cancellation during DROP INDEX#25733
Fix ISCP index consumer cancellation during DROP INDEX#25733jiangxinmeng1 wants to merge 18 commits into
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
LeftHandCold
left a comment
There was a problem hiding this comment.
Re-reviewed the latest head (4ad3702). The previous unbounded generation-fence retention is materially improved: async finalization now removes the exact fence, and table GC has a fallback cleanup. The focused ISCP and compile tests pass. Two correctness blockers remain, though: the new runner lookup still dereferences a process-local registry and therefore cannot drain a separate CN process, and the successful-commit callback removes the fence before the cancel record is guaranteed to be visible in the runner in-memory state. Please address the inline findings and replace the stubbed runner-routing test with a real cross-process/RPC or durable-handshake test.
|
This review is addressed by the latest changes. The destructive DDL path no longer depends on a process-local executor lookup on The generation fence lifetime is also handled for the remote path. The drain |
LeftHandCold
left a comment
There was a problem hiding this comment.
Re-reviewed current head dce285f. The queryservice RPC fixes the process-local cross-CN routing blocker. Two lifecycle blockers remain: the success fence is still removed before the drop logtail is visible, and remote rollback cleanup is a one-shot RPC whose failure can permanently fence a live job.
aptend
left a comment
There was a problem hiding this comment.
Re-reviewed the current head 1083581c61. The cross-CN drain routing and rollback recovery are materially improved, but the latest fence-lifetime change still leaves two generation-safety gaps, and the context-aware fault wait has an independent cross-waiter cancellation bug. Requesting changes for the following blockers:
-
[P1][generation safety] The success fence is removed before
dropAtis installed in memory.addOrUpdateJobcallsRemoveJobFenceatpkg/iscp/executor.go:975-977, then parses the row, looks up the table, and only installsdropAtthroughAddOrUpdateSinkerat line 1020. During that interval,TableEntry.getCandidatecan observe the old entry withdropAt == 0and no fence; both worker-side fence checks also see the fence removed, so the old generation can be admitted against hidden tables that the committed DDL has already dropped. I reproduced this deterministically by pausingaddOrUpdateJobafter fence removal but before the table lookup:getCandidatereturned the dropped job. Please install the terminal in-memory state before removing the exact-generation fence, preserve the fence on parse/update failure, and add a regression covering this handoff. -
[P1][generation safety] The fixed TTL reopens successful drops when logtail catch-up exceeds five minutes.
CancelAndDrainJobConsumerassigns every fenceDefaultRollbackFenceTTL, andisJobFencedLockeddeletes it after expiration regardless of transaction outcome. The TTL solves permanent retention after failed rollback cleanup, but it also applies to a successfully committed DROP. If the runner cannot apply thedrop_atlogtail within five minutes, the old in-memory job becomes schedulable again and the original write-after-drop race returns. Please distinguish rollback/unknown-outcome recovery from a successful-drop fence, or renew/reconcile the lease against durable state so success remains fenced untildropAtis actually installed. Add a success-with-stalled-logtail regression. -
[P2][fault correctness] Canceling one context-aware WAIT releases unrelated waiters. In
pkg/util/fault/fault.go:232-244, each waiter's context watcher calls the shared condition'sBroadcast(). With twoTriggerFaultWithContextcalls waiting on the same fault point, canceling only one context wakes both calls even though the second context is still live. A deterministic two-waiter regression fails immediately on this head. Please use per-waiter cancellation/registration or an equivalent predicate that does not turn one caller's cancellation into a global notify.
Validation on 1083581c61: focused tests for pkg/iscp, pkg/sql/compile, pkg/cnservice, pkg/queryservice/client, and pkg/util/fault pass; pkg/iscp and pkg/util/fault pass under -race, including focused -count=20 stress. The two deterministic regressions described above fail as expected and expose gaps not covered by the current tests. git diff --check passes.
Thanks for calling this out. I intentionally did not change the 5-minute fence TTL The fence is installed only after the foreground DROP has synchronously routed to the Making successful DROP fences non-expiring until The added regression covers this handoff: after |
LeftHandCold
left a comment
There was a problem hiding this comment.
Re-reviewed current head a08c234. The previous commit-to-logtail cleanup race and permanent rollback-fence leak are addressed, but the new fixed fence TTL can expire while a valid explicit DDL transaction is still open, reopening the consumer/hidden-table race this PR is intended to close.
aptend
left a comment
There was a problem hiding this comment.
Reviewed current head 70aba40f90. The latest change only extends the in-memory fence TTL from 5 to 30 minutes; it does not close the generation/terminal handoff gaps below. Requesting changes for three blockers:
-
[P1][generation safety] The fence is lost when the ISCP runner executor is replaced.
fencedJobsis process-local and every newISCPTaskExecutorstarts with an empty map. If the runner CN restarts or the daemon-task lease migrates while the DROP transaction is still open, the replacement executor replays the still-live durable job with no fence and can admit it against the hidden tables again. The lease cannot repair this: renewal is pinned to the originally resolved query address,RenewJobFencesilently does nothing when the key is absent, and the RPC handler still returns success. I reproduced this with a deterministic executor-replacement regression: the old executor is fenced, the replacement receives the current renewal call, andreplacement.IsJobFenced(key)is false. Please make the fence durable/reconstructible across runner generations (or explicitly hand it off/re-resolve and install it), make renewal fail when the fence is missing, and add a runner restart/lease-migration regression while the DDL transaction is unresolved. -
[P1][correctness] Renewal stops at commit entry, before either commit or the
drop_atlogtail handoff is complete. The non-costCommitEventinvokeslease.Stop(), buttxnOperator.Commitemits that event beforedoWrite. Therefore a slow/unknown commit or delayed logtail can outlive the remaining TTL;IsJobFencedthen deletes the fence while the in-memory job still hasdropAt == 0, reopening the original consumer/hidden-table race. Increasing the TTL to 30 minutes only delays this failure. The new test explicitly expects the fence to expire after commit starts, which codifies the unsafe contract. Remote renewal failures have the same problem: they are only logged, so an unresolved DDL silently becomes unfenced after the TTL. Please keep a successful-drop fence untiladdOrUpdateJobinstallsdropAt, remove/recover it only for rollback or a definitely failed commit, reconcile unknown outcomes, and make renewal failure fail closed. Add short injected-TTL regressions for commit-before-logtail, stalled logtail, and failed renewal. -
[P2][build reproducibility]
proto/query.protoandquery.pb.godisagree. The generated Go request contains and the implementation usesRenewFenceOnlyfield 6, butISCPDrainConsumerRequestinproto/query.protoends atRemoveFenceOnly = 5. Regenerating protobufs removes the field and breaks the two call sites. Please addbool RenewFenceOnly = 6;to the source proto and regeneratequery.pb.gowith the repository target.
Validation: existing focused tests for pkg/iscp, the drain-related pkg/sql/compile and pkg/cnservice cases, and pkg/util/fault -race pass; git diff --check passes. The deterministic runner-replacement regression described above fails on this head.
|
I fixed the proto source mismatch by adding For the durable fence suggestion, I agree that a purely in-memory fence cannot survive The current patch keeps the fence process-local and transaction-scoped, with renewal |
aptend
left a comment
There was a problem hiding this comment.
There is still a generation-safety gap in the cancellation fence.
The fence is stored only in the current executor's in-memory fencedJobs map. If the ISCP executor restarts or the daemon-task lease migrates while the DROP transaction is unresolved, the replacement executor starts with an empty fence map and can rebuild/admit the still-visible job (dropAt is not committed yet). The local lease also keeps renewing the old executor pointer.
The remote path has the same safety hole after restart or expiry: RenewJobFence silently does nothing when the exact fence is absent, while handleISCPDrainConsumer returns success. The DDL side therefore believes the fence is renewed even though the consumer can run again, reopening the DROP INDEX/consumer race this PR is intended to close.
Please make the fence durable or executor-generation/lease-owner aware, and require renewal to prove that the exact fence still exists (otherwise re-resolve the runner and re-drain/reinstall it before acknowledging success). The DDL must not commit if quiescence can no longer be proven. Please also add coverage for executor restart/task migration during an unresolved DDL transaction and renewal after the fence has expired or disappeared.
LeftHandCold
left a comment
There was a problem hiding this comment.
Found one remaining correctness issue in the remote fence renewal. The lease can still expire while a renewal RPC is in flight, and a late renewal is then silently acknowledged after the key has already been deleted, permanently losing the protection during an open DDL transaction. Requesting changes for the inline issue. I rechecked the consumer cancellation/drain, fan-out refcounting, rollback cleanup, and committed drop-logtail handoff; those paths look closed. The affected package tests pass.
I agree that an in-memory fence cannot provide a hard guarantee across runner restart For this PR, I intentionally kept the fence process-local and transaction-scoped. The Given that tradeoff, I would prefer to keep this PR scoped to the active-runner |
|
@jiangxinmeng1 Thanks, I agree that persisting the fence is not necessarily the right scope for this PR, and runner restart/daemon-task lease migration can be tracked as a separate lifecycle design. However, the current implementation still does not guarantee the active-runner lifecycle described here. No restart or migration is required: a renewal RPC starts at TTL/2 but may run for a full TTL. The existing fence can therefore expire while renewal is still in flight. Once This is the same active-runner case described in #25733 (comment), and does not require a durable fence. Please make renewal finish within the remaining lease with a safety margin, return an explicit error for a missing/expired fence, and ensure loss of the lease prevents the DDL from committing. With that closed, I am fine with documenting restart/migration as a separate follow-up. |
…umer-cancel # Conflicts: # pkg/pb/query/query.pb.go
LeftHandCold
left a comment
There was a problem hiding this comment.
Re-reviewed the latest head fc114288d7. The remaining fence-renewal failure mode can re-expose an intermittent DDL deadlock/retry window, but it does not establish a committed data-correctness failure: the initial consumer drain closes the active race, the destructive DDL changes are transactional, and consumer result updates do not clear drop_at. Treating renewal as best-effort avoids turning a transient control-plane failure into a guaranteed rollback of an otherwise valid transaction. I do not see a remaining blocker on this head.
@aptend That said, I agree the renewal ACK semantics must not be misleading. I have changed I would prefer not to add another pre-commit abort path in this PR unless we can show |
gouhongshen
left a comment
There was a problem hiding this comment.
Codex automated review
The fence-renewal failure path is not enforced, so a runner restart during an open DROP INDEX transaction can re-enable the consumer before the drop commits.
P1 - Abort or otherwise preserve quiescence when fence renewal fails (pkg/sql/compile/iscp_util.go:365)
Fence state is in-memory only. If the ISCP runner restarts (or the fence expires) while the DDL transaction remains open, RenewJobFence correctly returns false, but this goroutine only logs the error and continues. The replacement executor has no fence and RegisterRunningConsumer accepts the still-active, uncommitted job, allowing it to resume writes to the index being dropped. The new handler error for a missing fence is therefore never propagated to the DDL transaction. The renewal failure must keep the job fenced or cause the DROP transaction to fail/abort; logging is insufficient.
What type of PR is this?
Which issue(s) this PR fixes:
issue #25711
What this PR does / why we need it:
This PR fixes DROP INDEX and other destructive ISCP-backed index DDL paths by
canceling and draining the active index consumer before index metadata and hidden
tables are removed.
Changes:
Add ISCP runtime tracking for running job consumers.
Fence dropped index job generations so new iterations skip them while DDL is
waiting for quiescence.
Cancel the consumer context and data retriever, then wait for the running
consumer to exit.
Route DROP INDEX, ALTER TABLE DROP INDEX, DROP TABLE, and DROP DATABASE
through the ISCP drain path before deleting metadata or hidden tables.
Use the existing global ISCP daemon task runner UUID from sys_daemon_task as
the single ISCP executor owner.
Add a queryservice-based cross-CN drain request so a DDL CN can ask the actual
ISCP runner CN to cancel and drain the local executor.
Keep the local executor fast path for single-CN and same-runner cases.
Fail closed when quiescence cannot be confirmed, including missing query client,
missing runner query address, or remote drain RPC failure.
Clean remote generation fences through transaction commit/rollback callbacks.
Make data retrievers reject/cleanup queued data after cancellation.
Add ctx-aware fault injection for WAIT and SLEEP.
Fix WAIT fault removal so blocked waiters are notified, including the
TriggerFaultWithContext path.
Add ISCP cancel failpoints for different consumer phases, including
per-index-name injection.
Add unit tests for runtime cancel, retriever cleanup, fault cancellation,
local drain, remote queryservice drain, remote fence cleanup, and DDL drain
behavior.
Test Matrix:
make dev-upwith separatemo-cn1andmo-cn2mo-cn2; DDL was issued from non-runnermo-cn1.fj/iscp/cancel/after-register-consumerWAIT onfj/iscp/cancel/after-register-consumer.fj/iscp/cancel/after-submit.fj/iscp/cancel/fanout-before-send.fj/iscp/cancel/long/hnsw-before-update.fj/iscp/cancel/long/hnsw-before-save.fj/iscp/cancel/long/before-watermark.fj/iscp/cancel/long/before-execwith fulltext ASYNC.fj/iscp/cancel/long/before-send:idx01with sleep 30.drop_at.idx01.idx01removed;idx02preserved.make build.git diff --check.